#!/usr/bin/perl
#
# Copyright 2008-2010 Vadim Zhukov <persgray@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY AGAVA COMPANY ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AGAVA COMPANY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the AGAVA Company.

#######################################
# Base class for all rotate schemes. A scheme defines set of rules
#  for rotating backup. Each scheme have makeKeepDecision(),
#  which recieves list of all backup dates and choose ones to save.
#  Backup dates should be in "YYYYMMDD" format.
# Common init parameter is number of items to be saved.

package RB::RotateScheme;
use strict;

sub new {
	my $className = shift;
	my $this = {};
	$this->{'_maxLeft'} = shift;
	bless($this, $className);
	return $this;
}

sub maxLeft {
	my $this = shift;
	return $this->{'_maxLeft'};
}

#######################################
# Save backups on a one-per-month basis

package RB::RotateScheme::Month;
use strict;
@RB::RotateScheme::Month::ISA = qw/RB::RotateScheme/;

sub makeKeepDecision {
	my $this = shift;
	my $leftToFind = $this->{'_maxLeft'};
	my (%found, $m, $lastM);
	foreach my $date(sort { $b <=> $a } @_) {
		$m = substr($date, 0, 6);
		if ($leftToFind > 0 || $lastM eq $m) {
			unless (exists $found{$m}) {
				$leftToFind--;
			}
			$found{$m} = $date;
			$lastM = $m;
		}
	}
	return values %found;
}

#######################################
# Save backups on a one-per-week basis

package RB::RotateScheme::Week;
use strict;
@RB::RotateScheme::Week::ISA = qw/RB::RotateScheme/;

sub Week_of_Year {
	my ($year, $month, $day) = @_;
	my $t = mktime(0, 0, 0, $day-1, $month-1, $year-1900);
	my @t2 = gmtime($t);
	# [7] - yday
	my $week = $t2[7] / 7;
	# We do not need accurate result, just the same for every week
	return ($week, $year);
}

sub makeKeepDecision {
	my $this = shift;
	my $leftToFind = $this->{'_maxLeft'};
	my (@keep, %found, $date, $week, $year, $w, $lastW);
	foreach my $date (sort { $b <=> $a } @_) {
		($week, $year) = Week_of_Year(substr($date, 0, 4), substr($date, 4, 2), substr($date, 6, 2));
		$w = $year.$week;
		if ($leftToFind > 0 || $lastW eq $w) {
			unless (exists $found{$w}) {
				$leftToFind--;
			}
			$found{$w} = $date;
			$lastW = $w;
		}
	}
	return values %found;
}


#######################################
# Just save last N backups

package RB::RotateScheme::Day;
use strict;
@RB::RotateScheme::Day::ISA = qw/RB::RotateScheme/;

sub makeKeepDecision {
	my $this = shift;
	my $leftToFind = $this->{'_maxLeft'};
	my $count = scalar(@_);
	return @_ if $count <= $leftToFind;
	my @sorted = sort { $b <=> $a } @_;
	return splice (@sorted, 0, $leftToFind);
}


##############################################################################

package main;
use strict;
use Getopt::Std;
use RB::Profile;

$VERSION = "1.9";

#######################################
# Read configuration

my %CmdOptions;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
getopts('d:M:W:D:np:s:qv', \%CmdOptions);

my $profilePath;
if (exists $CmdOptions{'profile'}) {
	$profilePath = $CmdOptions{'profile'};
} else {
	$profilePath = '~/.rb/cpanel-mysql-only';
}

my $Profile = RB::Profile->new($profilePath);
my $Config = $Profile->{'global'};

$Config->{'archive_dir'} = $CmdOptions{'d'} || '~/com1';
$Config->{'test_mode'} = $CmdOptions{'n'};

$Config->{'verbosity'} = 0;
if ($CmdOptions{'v'}) {
	$Config->{'verbosity'}++;
} elsif ($CmdOptions{'q'}) {
	$Config->{'verbosity'}--;
}


#######################################

@RotateSchemes = ();
$moduleName = 'mysql';
my $module = $Profile->{$moduleName};
push ( @RotateSchemes, new RB::RotateScheme::Month($module->{'rotate_months'}) )
	if $module->{'rotate_months'};
push ( @RotateSchemes, new RB::RotateScheme::Week($module->{'rotate_weeks'}) )
	if $module->{'rotate_weeks'};
push ( @RotateSchemes, new RB::RotateScheme::Day($module->{'rotate_days'}) )
	if $module->{'rotate_days'};

chdir $Config->{'archive_dir'} or die "Cannot change working directory to ".$Config->{'archive_dir'};
opendir (BACKUPDIR, $Config->{'archive_dir'})
	or die "Cannot open archive directory: $!";
my %Servers;
while (my $f = readdir BACKUPDIR) {
	next if $f !~ /^([a-z0-9-]+(\.[a-z0-9-\.]+)?)_db_([0-9]{8})\.tgz$/;
	my $serverName = $1;
	$serverName .= '.agava.net' unless $2;
	$Servers{$serverName} = {} unless exists $Servers{$serverName};
	$Servers{$serverName}->{$3} = $f;
}
close BACKUPDIR;

foreach my $serverName (keys %Servers) {
	$Config->{'verbosity'} >= 1 and print STDOUT "Rotating backups for $serverName\n";
	my $server = $Servers{$serverName};
	my %keep;
	foreach my $scheme (@RotateSchemes) {
		foreach my $keepDate ($scheme->makeKeepDecision(keys %{$server})) {
			$keep{$keepDate} = 1;
		}
	}

	$Config->{'verbosity'} >= 1 and print STDOUT "Following backups will be kept for $serverName: ".
		join(', ', sort keys %keep)."\n";
	foreach my $date (keys %{$server}) {
		unless ($Config->{'test_mode'} or $keep{$date}) {
			#unlink $server->{$date};
			print STDOUT "Removing: ".$server->{$date}."\n";
		}
	}
}

exit 0;
